import { HttpError } from "../errors/http-error";
import { TimeoutError } from "../errors/timeout-error";
import { InterceptorType } from "../types/interceptor";
import { Input, InternalOptions, Options, SearchParamsInit } from "../types/options";
import { CenoPromise } from "../types/promise";
import { streamRequest, streamResponse } from "../utils/body";
import delay from "../utils/delay";
import { mergeHeaders } from "../utils/merge";
import { normalizeRequestMethod, normalizeRetryOptions } from "../utils/normalize";
import { findUnknownOptions } from "../utils/options";
import timeout, { TimeoutOptions } from "../utils/timeout";
import { ObjectEntries } from "../utils/types";
import { maxSafeTimeout, responseTypes, supportsAbortController, supportsFormData, supportsResponseStreams } from "./constants";
import { CenoInterceptor } from "./interceptor";

export class Ceno {
    static create<T = unknown>(input: Input, options: Options, interceptor?: CenoInterceptor): CenoPromise {
        const ceno = new Ceno(input, options, interceptor);

        const func = async (): Promise<Response> => {
            if (typeof ceno._options.timeout === 'number' && ceno._options.timeout > maxSafeTimeout) {
                throw new RangeError(`The \`timout\` option cannot be greater than ${maxSafeTimeout}`)
            }

            // 延迟获取，以便主体方法快捷方式可以设置 Accept 标头
            await Promise.resolve()

            ceno.request = await ceno._executeInterceptor('request', ceno.request)

            // 在使用 ceno.request 之前，_fetch 方法会克隆它并保存克隆对象，以便将来重试时使用。
            // 如果不需要重试，为了内存安全，管壁克隆请求的 ReadableStream 。
            let response = await ceno._fetch()

            response = await ceno._executeInterceptor('response', response)

            ceno._decorateResponse(response)

            if (!response.ok && ceno._options.throwHttpErrors) {
                let error = new HttpError(response, ceno.request)
                throw error
            }

            // 如果传入 `onDownloadProgress` ，则其内部会使用流 API 。
            if (ceno._options.onDownloadProgress) {
                if (typeof ceno._options.onDownloadProgress !== 'function') {
                    throw new TypeError('The `onDownloadProgress` option must be a function')
                }

                if (!supportsResponseStreams) {
                    throw new Error('Request streams are not supported in your environment. The `duplex` option for `Request` is not available.')
                }

                return streamResponse(response.clone(), ceno._options.onDownloadProgress)
            }

            return response
        }

        // 判断当前请求的方法是否属于可重试的方法列表中
        const isRetriableMethod = ceno._options.retry.methods.includes(ceno.request.method.toLowerCase())
        const result = (isRetriableMethod ? ceno._retry(func) : func())
            .finally(async () => {
                // 既然知道无需重试，那就关闭克隆请求的可读流
                if (!ceno.request.bodyUsed) {
                    await ceno.request.body?.cancel()
                }
            }) as CenoPromise

            for (const [type, mimeType] of Object.entries(responseTypes) as ObjectEntries<typeof responseTypes>) {
                (result as any)[type] = async () => {
                    ceno.request.headers.set('accept', ceno.request.headers.get('accept') || mimeType)
    
                    const response = await result
    
                    if (type === 'json') {
                        if (response.status === 204) {
                            return ''
                        }

                        const contentLength = response.headers.get('content-length')
                        if (contentLength === '0') {
                            return ''
                        }
    
                        if (options.parseJson) {
                            return options.parseJson(await response.text())
                        }
    
                        return response.json()
                    }
    
                    return (response as any)[type]()
                }
            }
    
            return result as CenoPromise<T>

    }

    public request: Request;
    protected abortController?: AbortController;
    protected _retryCount = 0;
    protected _input: Input;
    protected _options: InternalOptions;
    protected _interceptor?: CenoInterceptor;

    constructor(input: Input, options: Options = {}, interceptor?: CenoInterceptor) {
        this._input = input
        this._interceptor = interceptor

        this._options = {
            ...options,
            headers: mergeHeaders((this._input as Request).headers, options.headers),
            method: normalizeRequestMethod(options.method ?? (this._input as Request).method ?? 'GET'),
            prefixUrl: String(options.prefixUrl || ''),
            retry: normalizeRetryOptions(options.retry),
            throwHttpErrors: options.throwHttpErrors !== false,
            timeout: options.timeout ?? 10_000,
            fetch: options.fetch ?? global.fetch.bind(globalThis)
        }

        if (typeof this._input !== 'string' && !(this._input instanceof URL || this._input instanceof globalThis.URL)) {
            throw new TypeError('`input` must be a string, URL, or Request')
        }

        if (this._options.prefixUrl && typeof this._input === 'string') {
            if (this._input.startsWith('/')) {
                throw new Error('`input` must not begin with a slash when using `prefixUrl`')
            }

            if (!this._options.prefixUrl.endsWith('/')) {
                this._options.prefixUrl += '/'
            }

            this._input = this._options.prefixUrl + this._input
        }

        if (supportsAbortController) {
            const originalSignal = this._options.signal ?? (this._input instanceof Request ? this._input.signal : undefined)
            this.abortController = new globalThis.AbortController()
            this._options.signal = originalSignal ? AbortSignal.any([originalSignal, this.abortController.signal]) : this.abortController.signal
        }
        
        if (supportsResponseStreams) {
            // @ts-expect-error - 类型已经过时了。
            this._options.duplex = 'half'
        }

        if (this._options.json !== undefined) {
            this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json)
            this._options.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json')
        }

        this.request = new Request(this._input, this._options)

        if (this._options.searchParams) {
            const textSearchParams = typeof this._options.searchParams === 'string'
                ? this._options.searchParams
                : new URLSearchParams(this._options.searchParams as unknown as SearchParamsInit).toString()
            const searchParams = '?' + textSearchParams
            const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams)

            // 为了提供正确的表单边界，每次从另一个请求实例化新请求时，都应删除 Content-Type 标头。
            if (
                ((supportsFormData && this._options.body instanceof globalThis.FormData)
                    || this._options.body instanceof URLSearchParams) && !this._options.headers.get('content-type')
            ) {
                this.request.headers.delete('content-type')
            }

            // 由于某种原因，`this.request` 的传播是必需的，否则它会遗漏 “duplex” 选项并抛出异常。
            this.request = new globalThis.Request(new globalThis.Request(url, {...this.request}), this._options as RequestInit)
        }

        // 如果传入 `onUploadProgress` ，则其内部会使用流 API 。
        if (this._options.onUploadProgress) {
            if (typeof this._options.onUploadProgress !== 'function') {
                throw new TypeError('The `onUploadProgress` option must be a function')
            }

            if (!supportsResponseStreams) {
                throw new Error('Request streams are not supported in your environment. The `duplex` option for `Request` is not available.')
            }

            const originalBody = this.request.body
            if (originalBody) {
                this.request = streamRequest(this.request, this._options.onUploadProgress)
            }
        }
    }

    protected async _executeInterceptor<T extends 'request' | 'response'>(
        type: T, 
        value: InterceptorType<T>
    ): Promise<InterceptorType<T>> {
        let result = value


        // 全局拦截器
        if (this._options.interceptors?.[type]) {
            const interceptors = this._options.interceptors[type]
            const handlers = Array.isArray(interceptors) ? interceptors : [interceptors]
            for (const handler of handlers) {
                try {
                    if (handler.onFulfilled) {
                        result = await handler.onFulfilled(result)
                    }
                } catch (error) {
                    if (handler.onRejected) {
                        result = await handler.onRejected(error)
                    } else {
                        throw error
                    }
                }
            }
        }

        // 实例级拦截器（通过 interceptor.use() 添加的）
        if (this._interceptor) {
            result = await this._interceptor.execute(type, result);
        }

        return result
    }

    protected _calculateRetryDelay(error: unknown): number {
        this._retryCount++

        if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
            throw error
        }

        if (error instanceof HttpError) {
            if (!this._options.retry.statusCodes.includes(error.response.status)) {
                throw error
            }

            const retryAfter = error.response.headers.get('Retry-After')
                ?? error.response.headers.get('RateLimit-Reset')
                ?? error.response.headers.get('X-RateLimit-Reset')
                ?? error.response.headers.get('X-Rate-Limit-Reset')
            if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
                let after = Number(retryAfter) * 1000
                if (Number.isNaN(after)) {
                    after = Date.parse(retryAfter) - Date.now()
                } else if (after >= 7 * 24 * 60 * 60 * 1000) {
                    after -= Date.now()
                }

                const max = this._options.retry.maxRetryAfter ?? after
                return after < max ? after : max
            }
            
            if (error.response.status === 413) {
                throw error
            }
        }
        
        const retryDelay = this._options.retry.delay(this._retryCount)
        return Math.min(this._options.retry.backoffLimit, retryDelay)
    }

    protected _decorateResponse(response: Response): Response {
        if (this._options.parseJson) {
            response.json = async () => this._options.parseJson!(await response.text())
        }

        return response
    }

    protected async _retry<T extends (...arguments_: any) => Promise<any>>(func: T): Promise<ReturnType<T> | void> {
        while (true) {
            try {
                return await func()
            } catch (error) {
                const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout)
                if (this._retryCount < 1) {
                    throw error
                }

                await delay(ms, {signal: this._options.signal})
            }
        }
    }

    protected async _fetch(): Promise<Response> {
        const nonRequestOptions = findUnknownOptions(this.request, this._options)

        const mainRequest = this.request
        this.request = mainRequest.clone()

        if (this._options.timeout === false) {
            return this._options.fetch(mainRequest, nonRequestOptions)
        }

        return timeout(mainRequest, nonRequestOptions, this.abortController, this._options as TimeoutOptions)
    }
} 